在第六天,我們已經討論了 直接安裝 vs 鎖定版本,也學會在 pyproject.toml
的 [project].dependencies
裡,用 >=
、==
、~=
等 PEP 440 語法來表達浮動或鎖定。
但這樣做只能控制 主依賴,間接依賴仍然可能隨意升級,導致「昨天能跑、今天就壞」的情況。
因此,在 Python 世界裡,關於「可重現環境」有三個進化階段:
pyproject.toml
中直接宣告依賴範圍(工程化表達)。在早期,pip 沒有 lock 檔的概念,所以社群用 constraints.txt 來補強。
範例:
# constraints.txt
urllib3==2.2.3
charset-normalizer==3.3.2
安裝時帶入:
pip install -c constraints.txt requests
這樣即使 requests
間接依賴 urllib3
,也會被鎖在 2.2.3
。
👉 適合:
缺點:需要人工維護,而且只是「輔助規則」,不是完整鎖定。
隨著 pyproject.toml
成為標準,我們可以直接在 [project].dependencies
裡,用 PEP 440 語法表達鎖定範圍:
[project]
name = "awesome-app"
dependencies = [
"requests>=2.32,<3", # 區間鎖定
"python-dateutil==2.8.*", # 只允許 patch 升級
"numpy~=1.26" # 相容版號(允許 minor 升級)
]
這比 constraints.txt
更直觀,因為它 一開始就被納入專案設定,不用額外檔案。
再配合 [project.optional-dependencies]
,可同時管理開發工具與文件需求,並能跨 Hatch / pip / Poetry 重用。
雖然 pyproject.toml
已經能描述「想要的範圍」,但實際安裝時仍可能因間接依賴浮動而不一致。
這時候就需要 lock 檔。
在 Python 世界,新世代的解法就是 UV。
在這邊需要特別注意一下,我們將原本安裝套件使用的最原始的pip改成uv。
# 原始使用pip安裝依賴
#[tool.hatch.env.default]
#requires = ["hatch-pip-compile"]
[tool.hatch.envs.default]
installer = "uv"
uv lock # 產生 / 更新 uv.lock
uv sync --locked # 嚴格依照 uv.lock 安裝
uv lock --upgrade # 升級所有套件
uv lock --upgrade-package requests
執行 uv lock
指令會產生 uv.lock
檔案,裡面記錄了完整的依賴樹。往後在不同環境安裝時,只要透過 uv sync --locked
,就能確保版本完全一致。
![]
在 GitHub Actions 中:
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- name: Sync deps from lock
run: uv sync --locked
- name: Run tests
run: uv run pytest -q
這樣能確保 CI 環境與本地完全一致,不會再有「在我電腦能跑」的窘境。
即使有了 lock,每次 CI 都從零安裝依賴還是很耗時。
可以快取 .hatch
或 ~/.cache/uv
來加速:
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
項目 | Constraints | Hatch Dependencies | UV Lock |
---|---|---|---|
控制範圍 | 部分套件(補充規則) | 主依賴 + 範圍 | 全依賴樹 |
易用性 | 需額外檔案,人工維護 | pyproject.toml 直接宣告 | 自動生成 uv.lock |
重現性 | 部分一致 | 視間接依賴而定 | 完全一致 |
適用情境 | 臨時修補 / 白名單 | 專案需求表達 | 團隊工程化 / CI/CD |
就像前端專案使用 package-lock.json
或 yarn.lock
來確保一致性,Python 世界現在也能透過 uv.lock
達到相同目標。
今天我們看到了可重現環境的三個層次:
就像前端的 package-lock.json
/ yarn.lock
,Python 世界現在也有了自己的解法。
從 constraints.txt
到 pyproject.toml
的 dependencies,再到 uv.lock
,
我們正逐步走向一個更可靠、更工程化的 Python 生態 🚀。
明天我們將繼續探討 Day 8 - 一鍵化開發工作流:Hatch scripts × Nox